// Copyright (c) 2010 SuccessFactors, Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
//     * Redistributions of source code must retain the above
//       copyright notice, this list of conditions and the following
//       disclaimer.
//
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following
//       disclaimer in the documentation and/or other materials
//       provided with the distribution.
//
//     * Neither the name of the SuccessFactors, Inc. nor the names of
//       its contributors may be used to endorse or promote products
//       derived from this software without specific prior written
//       permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.

package org.owasp.jxt;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;

/**
 * JxtUtils
 *
 * @author Jeffrey Ichnowski
 * @version $Revision: 8 $
 */
public final class JxtUtils {

    private static final char[] HEX = "0123456789abcdef".toCharArray();
    private static final Encoder XML_SQ = new Encoder.XmlEncoder(
        Encoder.XmlEncoder.SINGLE_QUOTE);
    private static final Encoder XML_DQ = new Encoder.XmlEncoder(
        Encoder.XmlEncoder.DOUBLE_QUOTE);

    private JxtUtils() {}

    public static String capitalize(String name) {
        if (name == null || name.length() == 0) {
            throw new IllegalArgumentException("name must not be empty");
        }

        return Character.toUpperCase(name.charAt(0)) + name.substring(1);
    }

    public static String setterName(String property) {
        return "set" + capitalize(property);
    }

    @SuppressWarnings("unchecked")
    public static Iterator iterator(Object obj) {
        if (obj == null) {
            return Collections.EMPTY_LIST.iterator();
        } else if (obj instanceof Iterable) {
            return iterator((Iterable)obj);
        } else if (obj instanceof Object[]) {
            return iterator((Object[])obj);
        } else if (obj instanceof Iterator) {
            return (Iterator)obj;
        } else if (obj instanceof Enumeration) {
            return iterator((Enumeration)obj);
        } else {
            // single element list... might be better to throw an
            // exception
            return Arrays.asList(obj).iterator();
        }
    }

    public static <T> Iterator<T> iterator(T[] items) {
        return Arrays.asList(items).iterator();
    }

    public static <T> Iterator<T> iterator(Iterable<T> items) {
        return items.iterator();
    }

    public static <T> Iterator<T> iterator(Enumeration<T> e) {
        // Could do an EnumerationIterator class, but seems like more
        // effort than its worth for something that should be rarely
        // used.
        return Collections.list(e).iterator();
    }

    @SuppressWarnings("unchecked")
    public static <T> LoopIterator<T> loopIterator(Object obj) {
        return new LoopIterator<T>(iterator(obj));
    }

    public static <T> LoopIterator<T> loopIterator(T[] items) {
        return new LoopIterator<T>(iterator(items));
    }

    public static <T> LoopIterator<T> loopIterator(Iterable<T> items) {
        return new LoopIterator<T>(iterator(items));
    }

    public static <T> LoopIterator<T> loopIterator(Iterator<T> iter) {
        return new LoopIterator<T>(iter);
    }

    public static <T> LoopIterator<T> loopIterator(Enumeration<T> e) {
        return new LoopIterator<T>(iterator(e));
    }

    /**
     * Converts the argument to a quoted XML attribute.  This method
     * decides which quote character to use (" or ') to produce the
     * most readable output.  It favors the double-quote if the
     * outputs are equivalent.  The output of this is suitable for
     * embedding directly after the <code>=</code> of an attribute
     * name.  The output of this method is already surrounded by quote
     * characters and should NOT be surrounded by additional quotes.
     *
     * <p>For example, if <code>text</code> does not contain any
     * special characters the return value of this method is <code>'"'
     * + text + '"'</code>.
     *
     * @param text the attribute value to quote
     * @return the quoted attribute.  If text is null, a quoted empty
     * string is returned.
     */
    public static String quoteAttribute(String text) {
        if (text == null) {
            return "\"\"";
        }

        int n = text.length();
        int dCount = 0;
        int sCount = 0;

        for (int i=0 ; i<n ; ++i) {
            switch (text.charAt(i)) {
            case '\"':
                dCount++;
                break;
            case '\'':
                sCount++;
                break;
            default:
                break;
            }
        }

        StringBuilder buf = new StringBuilder(n*2);

        if (dCount <= sCount) {
            buf.append('\"');
            XML_DQ.apply(buf, text);
            buf.append('\"');
        } else {
            buf.append('\'');
            XML_SQ.apply(buf, text);
            buf.append('\'');
        }

        return buf.toString();
    }

    public static String unescapeXml(CharSequence text) {
        try {
            return unescapeXml(new StringBuilder(), text).toString();
        } catch (IOException e) {
            throw new InternalError();
        }
    }

    public static <T extends Appendable> T unescapeXml(T buf, CharSequence text) throws IOException {
        if (text != null) {
            int state = 0;
            int start = 0;
            int value = 0;
            int n = text.length();
            for (int i=0 ; i<n ; ++i) {
                char ch = text.charAt(i);

                switch (state) {
                case 0: // START
                    if (ch == '&') {
                        state = 1; // AMP
                        start = i;
                    } else {
                        buf.append(ch);
                    }
                    continue;
                case 1: // AMP
                    if (ch == '#') {
                        state = 2; // NUM
                    } else if ('a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z') {
                        value = ch;
                        state = 3; // ALPHA
                    } else {
                        break;
                    }
                    continue;
                case 2: // NUM
                    if ('0' <= ch && ch <= '9') {
                        state = 4; // DEC
                        value = ch - '0';
                    } else if (ch == 'x' || ch == 'X') {
                        state = 5; // HEX
                        value = 0;
                    } else {
                        break;
                    }
                    continue;
                case 4: // DEC
                    if (value > 0xffff) {
                        break; // overflow
                    }
                    if ('0' <= ch && ch <= '9') {
                        value = value * 10 + ch - '0';
                    } else if (ch == ';') {
                        buf.append((char)value);
                        state = 0; // START
                    } else {
                        break;
                    }
                    continue;
                case 5: // HEX
                    if (value > 0xffff) {
                        break; // overflow
                    }
                    if ('0' <= ch && ch <= '9') {
                        value = value * 16 + ch - '0';
                    } else if ('a' <= ch && ch <= 'f') {
                        value = value * 16 + ch - ('a' - 10);
                    } else if ('A' <= ch && ch <= 'F') {
                        value = value * 16 + ch - ('A' - 10);
                    } else if (ch == ';' && i-start > 3) {
                        buf.append((char)value);
                        state = 0; // START
                    } else {
                        break;
                    }
                    continue;
                case 3: // ALPHA
                    if ('a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z') {
                        // continue in this state
                        value = (value << 8) | ch;
                    } else if (ch == ';') {
                        state = 0;

                        // we only can accumulate 4 characters into
                        // value before it overflows.  if we have
                        // accumulated too many, return the value
                        // unescaped by breaking to the error handler.

                        if (i-start > 5) {
                            break;
                        }

                        switch (value) {
                        case (('l' << 8) | 't'):
                            buf.append('<');
                            continue;
                        case (('g' << 8) | 't'):
                            buf.append('>');
                            continue;
                        case (((('a' << 8) | 'm') << 8) | 'p'):
                            buf.append('&');
                            continue;
                        case (((((('a' << 8) | 'p') << 8) | 'o') << 8) | 's'):
                            buf.append('\'');
                            continue;
                        case (((((('q' << 8) | 'u') << 8) | 'o') << 8) | 't'):
                            buf.append('\"');
                            continue;
                        default:
                            break;
                        }

                    } else {
                        break;
                    }
                    continue;
                default:
                    throw new IllegalStateException();
                }

                // if we've reached here, that means the state machine
                // has kicked us out of the switch with a "break".
                // Here we return to the START state, flush everything
                // that we've seen since the last time we were in the
                // start state, and back up one character to reprocess
                // it (in case it is an '&').

                state = 0; // START
                buf.append(text, start, i);
                --i;

            } // for char loop

            if (state != 0) {
                buf.append(text, start, n);
            }
        }
        return buf;
    }

    public static String escape(String type, CharSequence text) {
        Encoder escape = Encoder.forName(type.toUpperCase());

        if (escape == null) {
            escape = Encoder.XML;
        }

        return escape.apply(text);
    }

    public static void appendAttribute(Appendable out, String key, Object value, String defaultValue)
        throws IOException
    {
        if (value == null) {
            // always treat null as though the attribute is not
            // specified.
            return;
        }

        // Treat boolean attributes as special.  If true, output the
        // key.  If false, treat it as null.
        if (value instanceof Boolean) {
            if (((Boolean)value).booleanValue()) {
                value = key;
            } else {
                return;
            }
        }

        String text = String.valueOf(value);

        if (defaultValue != null && defaultValue.equals(text)) {
            // do not output default values
            return;
        }

        out.append(' ');
        out.append(key);
        out.append('=');
        out.append(quoteAttribute(text));
    }

    public static void appendAttribute(PrintWriter out, String key, Object value, String defaultValue) {
        try {
            appendAttribute((Appendable)out, key, value, defaultValue);
        } catch (IOException e) {
            throw new UnsupportedOperationException(
                "Unexpected IOException", e);
        }
    }

    public static boolean equals(Object a, Object b) {
        return a != null && a.equals(b);
    }

    public static String resolveRelative(String base, String uri) {
        LinkedList<String> parts = new LinkedList<String>();

        if (!(uri.startsWith("/") || uri.startsWith("\\")) && base != null) {
            parts.addAll(Arrays.asList(base.split("[\\\\/]")));
            if (!parts.isEmpty() && !(base.endsWith("/") || base.endsWith("\\"))) {
                parts.removeLast();
            }
        }

        parts.addAll(Arrays.asList(uri.split("[\\\\/]")));

        // normalize
        ListIterator<String> iter = parts.listIterator();
        while (iter.hasNext()) {
            String part = iter.next();

            if (".".equals(part) || "".equals(part)) {
                iter.remove();
            } else if ("..".equals(part)) {
                iter.remove();
                if (iter.hasPrevious()) {
                    iter.previous();
                    iter.remove();
                }
            }
        }

        // join
        StringBuilder name = new StringBuilder();
        for (String part : parts) {
            name.append("/").append(part);
        }

        return name.toString();
    }

} // JxtUtils
